package top.yangguangmc.sunshine_anticheat.check;

import com.github.retrooper.packetevents.event.PacketListener;
import com.github.retrooper.packetevents.event.PacketListenerPriority;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.GameMode;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import top.yangguangmc.sunshine_anticheat.check.action.Action;
import top.yangguangmc.sunshine_anticheat.check.action.ActionType;
import top.yangguangmc.sunshine_anticheat.utils.Utils;

import java.util.*;
import java.util.logging.Level;

import static top.yangguangmc.sunshine_anticheat.utils.Builtin.*;

public abstract class Check implements Listener, PacketListener {
    public static final String BYPASS_PERMISSION = "sunshineac.bypass";
    private final String checkName;
    private final String configName;
    private final CheckCategory category;
    private final PacketListenerPriority priority;
    @CheckSetting(configName = "enabled")
    private boolean isEnabled = false;
    @CheckSetting(configName = "actions")
    private List<Action> actions = new LinkedList<>();
    @CheckSetting(configName = "action-delay")
    private int actionDelay = 0;
    private final Map<UUID, Integer> violations = new HashMap<>();
    private final Map<UUID, Map<Action, Integer>> actionsLastVl = new HashMap<>();

    public Check(String checkName, String configName, CheckCategory category) {
        this(checkName, configName, category, PacketListenerPriority.NORMAL);
    }

    public Check(String checkName, String configName, CheckCategory category, PacketListenerPriority priority) {
        this.checkName = checkName;
        this.configName = configName;
        this.category = category;
        this.priority = priority;
    }

    public String getCheckName() {
        return checkName;
    }

    public String getConfigName() {
        return configName;
    }

    public CheckCategory getCategory() {
        return category;
    }

    public PacketListenerPriority getPriority() {
        return priority;
    }

    public boolean isEnabled() {
        return isEnabled;
    }

    public void setEnabled(boolean enabled) {
        isEnabled = enabled;
    }

    public @Unmodifiable List<Action> getActions() {
        return Collections.unmodifiableList(actions);
    }

    void setActions(List<Action> actions) {
        this.actions = actions;
        actionsLastVl.clear();
    }

    public String getConfigKeyPath() {
        return "checks." + category.name().toLowerCase() + "." + configName;
    }

    public void onPluginDisable() {
    }

    public synchronized Map<UUID, Integer> getViolations() {
        return violations;
    }

    public synchronized int getViolation(Player player) {
        for (UUID uuid : violations.keySet()) {
            if (uuid.equals(player.getUniqueId())) {
                return violations.get(uuid);
            }
        }
        return 0;
    }

    public synchronized void setViolation(Player player, int vl, @Nullable String reason) {
        for (UUID uuid : violations.keySet()) {
            if (uuid.equals(player.getUniqueId())) {
                Integer vlFrom = violations.get(uuid);
                violations.put(uuid, vl);
                onViolationChange(player, vlFrom, vl, reason);
                return;
            }
        }
    }

    public void addViolation(Player player, int vl, @Nullable String reason) {
        setViolation(player, getViolation(player) + vl, reason);
    }

    public void resetViolation(Player player) {
        setViolation(player, 0, null);
        actionsLastVl.values().forEach(Map::clear);
        actionsLastVl.clear();
        onViolationReset(player);
    }

    public void resetViolations() {
        for (UUID uuid : violations.keySet()) {
            Player player = Bukkit.getPlayer(uuid);
            if (player != null) {
                resetViolation(player);
            }
        }
    }

    protected void onViolationChange(Player player, int vlFrom, int vlTo, @Nullable String reason) {
        boolean tellStaffs = false;
        actions.sort(null);
        for (Action action : actions) {
            actionsLastVl.putIfAbsent(player.getUniqueId(), new HashMap<>());
            Map<Action, Integer> map = actionsLastVl.get(player.getUniqueId());
            map.putIfAbsent(action, vlFrom);
            int lastVl = map.get(action);
            if (vlTo >= action.getBaseVL() && (lastVl < action.getBaseVL() || vlTo - lastVl >= action.getEveryVL())) {
                // perform action
                Utils.schedule(() -> {
                    switch (action.getType()) {
                        case CMD:
                            for (String cmd : action.getValue()) {
                                String commandLine = Utils.preprocessString(cmd, player);
                                if (ssac.isDebug() && player.hasPermission("sunshineac.tester") && commandLine.contains("kick")) {
                                    player.sendMessage(ssac.getPrefix() + "You were kicked by SSAC originally. As a tester, you will not be really kicked from the server.");
                                    player.sendMessage(ssac.getPrefix() + "To change this, you should remove your permission \"sunshineac.tester\".");
                                    Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "title " + player.getName() + " subtitle \"" + Utils.preprocessString(reason, player) + "\"");
                                    Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "title " + player.getName() + " title \"" + ChatColor.RED + "[SSAC] Kicked\"");
                                } else
                                    Bukkit.dispatchCommand(Bukkit.getConsoleSender(), commandLine);
                            }
                            break;
                        case PLAYER_CMD:
                            for (String cmd : action.getValue())
                                Bukkit.dispatchCommand(player, Utils.preprocessString(cmd, player));
                            break;
                        case MSG:
                            for (String line : action.getValue())
                                player.sendMessage(Utils.preprocessString(line, player));
                            break;
                        case ALERT:
                            if (action.getValue().isEmpty() || action.getValue().stream().allMatch(s -> s.trim().isEmpty()))
                                alert(player, vlFrom, vlTo, reason);
                            else action.getValue().forEach(s -> ssac.getAlertManager().send(s));
                            break;
                        case BROADCAST:
                            for (String line : action.getValue())
                                Bukkit.broadcastMessage(Utils.preprocessString(line, player));
                            break;
                    }
                }, actionDelay);
                if (action.getType() != ActionType.ALERT) tellStaffs = true;
                map.put(action, vlTo);
            }
        }
        if (tellStaffs)
            ssac.getAlertManager().send(ChatColor.GOLD + player.getName() + ChatColor.RED + " was punished for hacking! "
                    + ChatColor.GRAY + "(by " + checkName + " with VL:" + vlTo + ")");
    }

    @SuppressWarnings({"unused", "SameParameterValue"})
    protected void debugLog(String msg, Object... args) {
        if (!ssac.isDebug()) return;
        int i = msg.indexOf("{}");
        int j = 0;
        while (i != -1) {
            msg = msg.replaceFirst("\\{}", String.valueOf(args[j]));
            i = msg.indexOf("{}");
            j++;
        }
        logger.warning("[" + getCheckName() + "] " + msg);
        for (Player player : Bukkit.getOnlinePlayers()) {
            if (player.hasPermission("sunshineac.alert") || player.hasPermission("sunshineac.tester"))
                player.sendMessage(ssac.getPrefix() + "[DEBUG][" + getCheckName() + "] " + ChatColor.GRAY + msg);
        }
    }

    /**
     * Validates if the {@code expectedPlayer} is a player, and can be checked.
     *
     * @param expectedPlayer The entity that you'd like to be a player.
     */
    @SuppressWarnings("RedundantIfStatement")
    protected boolean validateCheckable(Entity expectedPlayer) {
        if (!isEnabled) return false;
        if (!(expectedPlayer instanceof Player)) return false;
        Player player = (Player) expectedPlayer;
//        if (!player.isOnline()) return false;
        if (player.getGameMode() == GameMode.CREATIVE || player.getGameMode() == GameMode.SPECTATOR) return false;
        if (player.hasPermission(BYPASS_PERMISSION)) return false;
        if (player.hasPermission(BYPASS_PERMISSION + ".*")) return false;
        if (player.hasPermission(BYPASS_PERMISSION + "." + configName)) return false;
        return true;
    }

    public void failCheck(Player player, int vl, String description, Object... args) {
        try {
            int i = description.indexOf("{}");
            int j = 0;
            while (i != -1) {
                description = description.replaceFirst("\\{\\}", String.valueOf(args[j]));
                i = description.indexOf("{}");
                j++;
            }
        } catch (IndexOutOfBoundsException | IllegalFormatException e) {
            description = ChatColor.RED + "" + ChatColor.BOLD + "[Internal Error]: " + ChatColor.RED + description;
            logger.log(Level.WARNING, "Error while formating check failure message!", e);
        }
        addViolation(player, vl, description);
    }

    public void alert(Player player, int vlFrom, int vlTo, String description) {
        ssac.getAlertManager().send(ssac.getConfigManager().getString("alert-message", player)
                .replace("%player%", player.getName())
                .replace("%check_category%", category.name().toLowerCase())
                .replace("%check_name%", checkName)
                .replace("%vl%", String.valueOf(vlTo))
                .replace("%delta_vl%", String.valueOf(vlTo - vlFrom))
                .replace("%description%", Utils.preprocessString(description, player)));
    }

    protected void onViolationReset(Player player) {
    }
}
